(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
(標題致敬JSDC工作坊的主題XD)
前面在講為什麼今年這麼多人推薦React hook時,我只有提說它讓簡單的元件可以不用class component的方式寫。實際上還有一個很重要的原因。
它能讓我們在class component常用的React特性被模組化。
什麼意思?為什麼好?
在物件導向程式語言中,我們在講到使用class的好處時,通常會提到它能夠讓變數和函式被模組化。也就是當我們宣告並使用多個相同class的物件時,這些物件雖然結構相同,但是會獨立運作,其中的變數、函式不受其他實體的同class物件影響(不論static)。
但是React在這點卻遇到了一個問題: 當我們想要在多個地方使用有相同結構的React特性時(如:state、生命週期等),如果要使每個都獨立運作不受彼此影響,就要直接多做出一個class component,並沒有其他更簡易的模組化方法。這樣程式碼不但笨重,而且不符合直覺。
例如當我們想在多個地方使用一個純用來以特殊邏輯計算次數的計次器Counter,即使我們只是要在不同地方讓不同計次器能獨立的回傳「次數」這個資料,並沒有要它在DOM上渲染什麼東西,卻還是要以class component撰寫,且要在父元件中使用<Counter/>
。
custom hook的出現為類似的問題找到了解答。
「創造自己的React hook」
這是custom hook的定義。最常用來讓其他hook互相搭配、組合後,把React的特性模組化,在多個function component中使用。
你可以在custom hook中創造state、生命週期......等,而對於同樣但呼叫的元件、順序不同的custom hook而言,他們彼此是獨立的。
聽不太懂捏?
等等看實例比較好了解。
Without it, we wouldn’t be able to automatically check for violations of rules of Hooks because we couldn’t tell if a certain function contains calls to Hooks inside of it.
沒了。
啥? 這樣就沒有了? 你什麼都沒有講啊?
對,它的寫法就是普通的函式,你要定義什麼、回傳什麼都可以。其它的語法規定都跟React hook的規定一樣,像是只能在function component中呼叫、不能在if-else中定義......等,我們在【React.js入門 - 13】 useState - 在function component用state和【React.js入門 - 20】 useEffect - 在function component用生命週期都提過。
還是看不懂就來看Custom hook實例吧!
我們在【React.js入門 - 23】 元件練習(下) - 在function利用useEffect遞迴+useState實作動畫的程式碼中做了一個「動態漸變的進度條」。
import React, { useState,useEffect,useRef,Component} from 'react';
const ProgressDIY=(props)=>{
const [percent,setPercent]=useState(10);
const mounted=useRef();
const tm=useRef();
const tmTwo=useRef();
useEffect(()=>{
if(!mounted.current){//componentDidMount
setPercent(props.value);
mounted.current=true;
}
else{ //componentDidUpdate
if(percent>props.value){
if(tm.current)
clearTimeout(tm.current)
tmTwo.current=setTimeout(()=>{setPercent(percent-1)},20);
}
else if(percent<props.value){
if(tmTwo.current)
clearTimeout(tmTwo.current)
tm.current=setTimeout(()=>{setPercent(percent+1)},20);
}
}
},[props.value,percent]);
return(
<div>
<div className="progress-back" style={{backgroundColor:"rgba(0,0,0,0.2)",width:"200px",height:"7px",borderRadius:"10px"}}>
<div className="progress-bar" style={{backgroundColor:"#fe5196",width:percent.toString()+"%",height:"100%",borderRadius:"10px"}}></div>
</div>
目前比率: {percent}%
<button value={90} onClick={props.onClick}>增加比率至90</button>
<button value={10} onClick={props.onClick}>減少比率至10</button>
</div>
)
}
export default ProgressDIY;
現在我們要把其中「漸變數字」這個功能模組化:
const [percent,setPercent]=useState(10);
const mounted=useRef();
const tm=useRef();
const tmTwo=useRef();
useEffect(()=>{
if(!mounted.current){//componentDidMount
setPercent(props.value);
mounted.current=true;
}
else{ //componentDidUpdate
if(percent>props.value){
if(tm.current)
clearTimeout(tm.current)
tmTwo.current=setTimeout(()=>{setPercent(percent-1)},20);
}
else if(percent<props.value){
if(tmTwo.current)
clearTimeout(tmTwo.current)
tm.current=setTimeout(()=>{setPercent(percent+1)},20);
}
}
},[props.value,percent]);
做出一個這樣的函式(hook):
然後同時在ProgressDIY和一個完全無關的新元件中使用,並且兩者的運作要是獨立的。我們會專注在寫custom hook上,新的元件是直接用我寫好的。
因為我們希望有一個輸入數字,所以要在函式接收一個參數。
const useRate=(value)=>{
}
export default useRate;
因為要實現漸變,我們需要state、componentDidMount跟componentDidUpdate,所以和上一篇要引入的hook相同:
import { useState,useEffect,useRef} from 'react';
另外因為custom hook不是component,不用引入React。
對。你可以直接剪下貼過來,useEffect監控值改成value就好。
這樣等於是我們在custom hook中,建立了屬於custom hook的state和生命週期,並且把接到的value以上一篇的邏輯做運算後,給予state新的值。
和之前ProgressDIY有什麼差別呢?
差別在原本的ProgressDIY除了在不同時期去做state運算,還要渲染元素到畫面上,成為一個元件。而現在我們等於讓「在不同時期去做state運算」這件事情給custom hook去處理,ProgressDIY改專心渲染元素就好。 有點像是從原本連磨麵粉都自己做的日式麵包店,開始把磨麵粉的工作包給外面廠商的感覺。之後即使有另外一個人開了歐式的麵包店,因為麵粉這個材料是相同的,所以他也可以跟這家廠商買。
為了要方便分辨,我這裡把所有percent的名稱改成rate。不過你也可以直接沿用percent這個名稱。
const useRate=(value)=>{
const [rate,setRate]=useState(0);
const mounted=useRef();
const tm=useRef();
const tmTwo=useRef();
useEffect(()=>{
if(!mounted.current){ //componentDidMount
setRate(value);
mounted.current=true;
}
else{ //componentDidUpdate
if(rate>value){
if(tm.current)
clearTimeout(tm.current)
tmTwo.current=setTimeout(()=>{setRate(rate-1)},20);
}
else if(rate<value){
if(tmTwo.current)
clearTimeout(tmTwo.current)
tm.current=setTimeout(()=>{setRate(rate+1)},20);
}
}
},[value,rate]);
}
在函式的最後面加上return rate;
(請注意我是用改過的名稱,它就是我們原本在ProgressDIY中的percent),讓使用這個hook的人可以透過回傳值接收到我們經過運算之後的新state,也就是漸變值。
const useRate=(value)=>{
const [rate,setRate]=useState(0);
const mounted=useRef();
const tm=useRef();
const tmTwo=useRef();
useEffect(()=>{
/* 略 */
}
},[value,rate]);
return rate;
}
到這邊,我們的custom hook就做好了。
import useRate from './useRate.js';
因為useRate已經會回傳「目前漸變的值」,所以我們不需要再在ProgressDIY中做漸變的動作,只需要用一個變數去接它傳出來的值就好。記得把props.value丟到useRate的參數。
import React from 'react';
import useRate from './useRate.js';
const ProgressDIY=(props)=>{
const percent=useRate(props.value);
return(
<div>
<div className="progress-back" style={{backgroundColor:"rgba(0,0,0,0.2)",width:"200px",height:"7px",borderRadius:"10px"}}>
<div className="progress-bar" style={{backgroundColor:"#fe5196",width:percent.toString()+"%",height:"100%",borderRadius:"10px"}}></div>
</div>
目前比率: {percent}%
<button value={90} onClick={props.onClick}>增加比率至90</button>
<button value={10} onClick={props.onClick}>減少比率至10</button>
</div>
)
}
到這邊,ProgressDIY已經可以和之前一模一樣的運作。
現在,請再建立一個Cheer.js。 並加入以下的程式碼
import React from 'react';
const Cheer=(props)=>{
return (
<div>
<h1>美心加油器</h1>
<div>目前分數{props.value}<br/>還有沒有! 再來{88-props.value}分!</div>
<button value={Number(props.value)+1} onClick={props.onClick}>加1分</button>
<button value={Number(props.value)+7} onClick={props.onClick}>加7分</button>
<button value={Number(props.value)+10} onClick={props.onClick}>加10分</button>
<button value={0} onClick={props.onClick}>0分</button>
</div>
)
}
export default Cheer;
這個程式碼的邏輯很簡單,就是我們會用props去接收並在螢幕顯示目前的分數,如果使用者覺得分數不夠高,可以點擊加1/7/10分。如果使用者覺得分數太高,可以讓分數歸0。
import React, {useState} from 'react';
import ProgressDIY from './ProgressDIY';
import Cheer from './Cheer';
const App=()=>{
const [value,setValue]=useState(0);
return(
<div id="App">
<ProgressDIY value={value} onClick={(e)=>{setValue(e.target.value)}}/>
<Cheer/>
</div>
)
}
不過這樣Cheer還是不能運作,因為它需要一個props來控制他的值。
import React, {useState} from 'react';
import ProgressDIY from './ProgressDIY';
import Cheer from './Cheer';
const App=()=>{
const [value,setValue]=useState(0);
const [score,setScore]=useState(0);
return(
<div id="App">
<ProgressDIY value={value} onClick={(e)=>{setValue(e.target.value)}}/>
<Cheer value={score} onClick={(e)=>{setScore(e.target.value)}}/>
</div>
)
}
到這邊,我們已經可以來看現在Cheer.js在幹嘛了。
上面是剛剛的Progresss,下面是剛剛引入的Cheer.js。
跟剛剛說的一樣,它是一個會顯示目前分數、顯示離88分的分數、可以操控分數的一個計分器。
如果在這裡也要讓分數漸變,是不是又要再寫一次生命週期跟state呢? 答案是不用,因為我們剛剛寫好了「會輸出朝著輸入目標漸變的數字」的custom hook。
import useRate from './useRate.js';
記得要把設定值props.value丟入參數。
const score=useRate(props.value);
把div中兩個用來顯示分數的值改成score
import React from 'react';
import usePercent from './usePercent.js';
const Cheer=(props)=>{
const score=useRate(props.value);
return (
<div>
<h1>美心加油器</h1>
<div>目前分數{score}<br/>還有沒有! 再來{88-score}分!</div>
<button value={Number(props.value)+1} onClick={props.onClick}>加1分</button>
<button value={Number(props.value)+7} onClick={props.onClick}>加7分</button>
<button value={Number(props.value)+10} onClick={props.onClick}>加10分</button>
<button value={0} onClick={props.onClick}>0分</button>
</div>
)
}
export default Cheer;
執行結果:
注意到了嗎? 在這個練習中,兩個元件雖然都使用了useRate這個函式。但是兩個接收到的值不受彼此影響,也就是custom hook在兩者中的state和生命週期是獨立運作的。而且我們在兩個元件都沒有寫出漸變的函式,只是引入模組化好的useRate。原本在ProgressDIY我們光寫漸變就花了十幾行。現在我們因為做了模組化,即使兩個元件都要漸變,他們卻都省掉了漸變所需要用的那十幾行,可讀性大幅的提高,也少去重複寫的麻煩。
這就是另一個大家超推React hook的理由。
因為本系列是重點是React入門,有關React hook的介紹就到這邊告一個段落。其他像是useContext、useRef、useReducer...還有很多好用的React hook,可以參考目前鐵人賽許多以React hook以主體的系列,當你習慣後一定會有不少收穫。